( function () {
class NRRDLoader extends THREE.Loader {
  constructor(manager) {
    super(manager);
  }

  load(url, onLoad, onProgress, onError) {
    const scope = this;
    const loader = new THREE.FileLoader(scope.manager);
    loader.setPath(scope.path);
    loader.setResponseType('arraybuffer');
    loader.setRequestHeader(scope.requestHeader);
    loader.setWithCredentials(scope.withCredentials);
    loader.load(url, function (data) {
      try {
        onLoad(scope.parse(data));
      } catch (e) {
        if (onError) {
          onError(e);
        } else {
          console.error(e);
        }

        scope.manager.itemError(url);
      }
    }, onProgress, onError);
  }

  parse(data) {
    // this parser is largely inspired from the XTK NRRD parser : https://github.com/xtk/X
    let _data = data;
    let _dataPointer = 0;

    const _nativeLittleEndian = new Int8Array(new Int16Array([1]).buffer)[0] > 0;

    const _littleEndian = true;
    const headerObject = {};

    function scan(type, chunks) {
      if (chunks === undefined || chunks === null) {
        chunks = 1;
      }

      let _chunkSize = 1;
      let _array_type = Uint8Array;

      switch (type) {
        // 1 byte data types
        case 'uchar':
          break;

        case 'schar':
          _array_type = Int8Array;
          break;
        // 2 byte data types

        case 'ushort':
          _array_type = Uint16Array;
          _chunkSize = 2;
          break;

        case 'sshort':
          _array_type = Int16Array;
          _chunkSize = 2;
          break;
        // 4 byte data types

        case 'uint':
          _array_type = Uint32Array;
          _chunkSize = 4;
          break;

        case 'sint':
          _array_type = Int32Array;
          _chunkSize = 4;
          break;

        case 'float':
          _array_type = Float32Array;
          _chunkSize = 4;
          break;

        case 'complex':
          _array_type = Float64Array;
          _chunkSize = 8;
          break;

        case 'double':
          _array_type = Float64Array;
          _chunkSize = 8;
          break;
      } // increase the data pointer in-place


      let _bytes = new _array_type(_data.slice(_dataPointer, _dataPointer += chunks * _chunkSize)); // if required, flip the endianness of the bytes


      if (_nativeLittleEndian != _littleEndian) {
        // we need to flip here since the format doesn't match the native endianness
        _bytes = flipEndianness(_bytes, _chunkSize);
      }

      if (chunks == 1) {
        // if only one chunk was requested, just return one value
        return _bytes[0];
      } // return the byte array


      return _bytes;
    } //Flips typed array endianness in-place. Based on https://github.com/kig/DataStream.js/blob/master/DataStream.js.


    function flipEndianness(array, chunkSize) {
      const u8 = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);

      for (let i = 0; i < array.byteLength; i += chunkSize) {
        for (let j = i + chunkSize - 1, k = i; j > k; j--, k++) {
          const tmp = u8[k];
          u8[k] = u8[j];
          u8[j] = tmp;
        }
      }

      return array;
    } //parse the header


    function parseHeader(header) {
      let data, field, fn, i, l, m, _i, _len;

      const lines = header.split(/\r?\n/);

      for (_i = 0, _len = lines.length; _i < _len; _i++) {
        l = lines[_i];

        if (l.match(/NRRD\d+/)) {
          headerObject.isNrrd = true;
        } else if (l.match(/^#/)) {} else if (m = l.match(/(.*):(.*)/)) {
          field = m[1].trim();
          data = m[2].trim();
          fn = _fieldFunctions[field];

          if (fn) {
            fn.call(headerObject, data);
          } else {
            headerObject[field] = data;
          }
        }
      }

      if (!headerObject.isNrrd) {
        throw new Error('Not an NRRD file');
      }

      if (headerObject.encoding === 'bz2' || headerObject.encoding === 'bzip2') {
        throw new Error('Bzip is not supported');
      }

      if (!headerObject.vectors) {
        //if no space direction is set, let's use the identity
        headerObject.vectors = [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1)]; //apply spacing if defined

        if (headerObject.spacings) {
          for (i = 0; i <= 2; i++) {
            if (!isNaN(headerObject.spacings[i])) {
              headerObject.vectors[i].multiplyScalar(headerObject.spacings[i]);
            }
          }
        }
      }
    } //parse the data when registred as one of this type : 'text', 'ascii', 'txt'


    function parseDataAsText(data, start, end) {
      let number = '';
      start = start || 0;
      end = end || data.length;
      let value; //length of the result is the product of the sizes

      const lengthOfTheResult = headerObject.sizes.reduce(function (previous, current) {
        return previous * current;
      }, 1);
      let base = 10;

      if (headerObject.encoding === 'hex') {
        base = 16;
      }

      const result = new headerObject.__array(lengthOfTheResult);
      let resultIndex = 0;
      let parsingFunction = parseInt;

      if (headerObject.__array === Float32Array || headerObject.__array === Float64Array) {
        parsingFunction = parseFloat;
      }

      for (let i = start; i < end; i++) {
        value = data[i]; //if value is not a space

        if ((value < 9 || value > 13) && value !== 32) {
          number += String.fromCharCode(value);
        } else {
          if (number !== '') {
            result[resultIndex] = parsingFunction(number, base);
            resultIndex++;
          }

          number = '';
        }
      }

      if (number !== '') {
        result[resultIndex] = parsingFunction(number, base);
        resultIndex++;
      }

      return result;
    }

    const _bytes = scan('uchar', data.byteLength);

    const _length = _bytes.length;
    let _header = null;
    let _data_start = 0;
    let i;

    for (i = 1; i < _length; i++) {
      if (_bytes[i - 1] == 10 && _bytes[i] == 10) {
        // we found two line breaks in a row
        // now we know what the header is
        _header = this.parseChars(_bytes, 0, i - 2); // this is were the data starts

        _data_start = i + 1;
        break;
      }
    } // parse the header


    parseHeader(_header);
    _data = _bytes.subarray(_data_start); // the data without header

    if (headerObject.encoding.substring(0, 2) === 'gz') {
      // we need to decompress the datastream
      // here we start the unzipping and get a typed Uint8Array back
      _data = fflate.gunzipSync(new Uint8Array(_data)); // eslint-disable-line no-undef
    } else if (headerObject.encoding === 'ascii' || headerObject.encoding === 'text' || headerObject.encoding === 'txt' || headerObject.encoding === 'hex') {
      _data = parseDataAsText(_data);
    } else if (headerObject.encoding === 'raw') {
      //we need to copy the array to create a new array buffer, else we retrieve the original arraybuffer with the header
      const _copy = new Uint8Array(_data.length);

      for (let i = 0; i < _data.length; i++) {
        _copy[i] = _data[i];
      }

      _data = _copy;
    } // .. let's use the underlying array buffer


    _data = _data.buffer;
    const volume = new THREE.Volume();
    volume.header = headerObject; //
    // parse the (unzipped) data to a datastream of the correct type
    //

    volume.data = new headerObject.__array(_data); // get the min and max intensities

    const min_max = volume.computeMinMax();
    const min = min_max[0];
    const max = min_max[1]; // attach the scalar range to the volume

    volume.windowLow = min;
    volume.windowHigh = max; // get the image dimensions

    volume.dimensions = [headerObject.sizes[0], headerObject.sizes[1], headerObject.sizes[2]];
    volume.xLength = volume.dimensions[0];
    volume.yLength = volume.dimensions[1];
    volume.zLength = volume.dimensions[2]; // spacing

    const spacingX = new THREE.Vector3(headerObject.vectors[0][0], headerObject.vectors[0][1], headerObject.vectors[0][2]).length();
    const spacingY = new THREE.Vector3(headerObject.vectors[1][0], headerObject.vectors[1][1], headerObject.vectors[1][2]).length();
    const spacingZ = new THREE.Vector3(headerObject.vectors[2][0], headerObject.vectors[2][1], headerObject.vectors[2][2]).length();
    volume.spacing = [spacingX, spacingY, spacingZ]; // Create IJKtoRAS matrix

    volume.matrix = new THREE.Matrix4();
    let _spaceX = 1;
    let _spaceY = 1;
    const _spaceZ = 1;

    if (headerObject.space == 'left-posterior-superior') {
      _spaceX = -1;
      _spaceY = -1;
    } else if (headerObject.space === 'left-anterior-superior') {
      _spaceX = -1;
    }

    if (!headerObject.vectors) {
      volume.matrix.set(_spaceX, 0, 0, 0, 0, _spaceY, 0, 0, 0, 0, _spaceZ, 0, 0, 0, 0, 1);
    } else {
      const v = headerObject.vectors;
      volume.matrix.set(_spaceX * v[0][0], _spaceX * v[1][0], _spaceX * v[2][0], 0, _spaceY * v[0][1], _spaceY * v[1][1], _spaceY * v[2][1], 0, _spaceZ * v[0][2], _spaceZ * v[1][2], _spaceZ * v[2][2], 0, 0, 0, 0, 1);
    }

    volume.inverseMatrix = new THREE.Matrix4();
    volume.inverseMatrix.copy(volume.matrix).invert();
    volume.RASDimensions = new THREE.Vector3(volume.xLength, volume.yLength, volume.zLength).applyMatrix4(volume.matrix).round().toArray().map(Math.abs); // .. and set the default threshold
    // only if the threshold was not already set

    if (volume.lowerThreshold === -Infinity) {
      volume.lowerThreshold = min;
    }

    if (volume.upperThreshold === Infinity) {
      volume.upperThreshold = max;
    }

    return volume;
  }

  parseChars(array, start, end) {
    // without borders, use the whole array
    if (start === undefined) {
      start = 0;
    }

    if (end === undefined) {
      end = array.length;
    }

    let output = ''; // create and append the chars

    let i = 0;

    for (i = start; i < end; ++i) {
      output += String.fromCharCode(array[i]);
    }

    return output;
  }

}

const _fieldFunctions = {
  type: function (data) {
    switch (data) {
      case 'uchar':
      case 'unsigned char':
      case 'uint8':
      case 'uint8_t':
        this.__array = Uint8Array;
        break;

      case 'signed char':
      case 'int8':
      case 'int8_t':
        this.__array = Int8Array;
        break;

      case 'short':
      case 'short int':
      case 'signed short':
      case 'signed short int':
      case 'int16':
      case 'int16_t':
        this.__array = Int16Array;
        break;

      case 'ushort':
      case 'unsigned short':
      case 'unsigned short int':
      case 'uint16':
      case 'uint16_t':
        this.__array = Uint16Array;
        break;

      case 'int':
      case 'signed int':
      case 'int32':
      case 'int32_t':
        this.__array = Int32Array;
        break;

      case 'uint':
      case 'unsigned int':
      case 'uint32':
      case 'uint32_t':
        this.__array = Uint32Array;
        break;

      case 'float':
        this.__array = Float32Array;
        break;

      case 'double':
        this.__array = Float64Array;
        break;

      default:
        throw new Error('Unsupported NRRD data type: ' + data);
    }

    return this.type = data;
  },
  endian: function (data) {
    return this.endian = data;
  },
  encoding: function (data) {
    return this.encoding = data;
  },
  dimension: function (data) {
    return this.dim = parseInt(data, 10);
  },
  sizes: function (data) {
    let i;
    return this.sizes = function () {
      const _ref = data.split(/\s+/);

      const _results = [];

      for (let _i = 0, _len = _ref.length; _i < _len; _i++) {
        i = _ref[_i];

        _results.push(parseInt(i, 10));
      }

      return _results;
    }();
  },
  space: function (data) {
    return this.space = data;
  },
  'space origin': function (data) {
    return this.space_origin = data.split('(')[1].split(')')[0].split(',');
  },
  'space directions': function (data) {
    let f, v;
    const parts = data.match(/\(.*?\)/g);
    return this.vectors = function () {
      const _results = [];

      for (let _i = 0, _len = parts.length; _i < _len; _i++) {
        v = parts[_i];

        _results.push(function () {
          const _ref = v.slice(1, -1).split(/,/);

          const _results2 = [];

          for (let _j = 0, _len2 = _ref.length; _j < _len2; _j++) {
            f = _ref[_j];

            _results2.push(parseFloat(f));
          }

          return _results2;
        }());
      }

      return _results;
    }();
  },
  spacings: function (data) {
    let f;
    const parts = data.split(/\s+/);
    return this.spacings = function () {
      const _results = [];

      for (let _i = 0, _len = parts.length; _i < _len; _i++) {
        f = parts[_i];

        _results.push(parseFloat(f));
      }

      return _results;
    }();
  }
};

THREE.NRRDLoader = NRRDLoader;
} )();
